/* Copyright (c) 2020 XEPIC Corporation Limited */
/*
 * Copyright (c) 1999-2019 Stephen Williams (steve@icarus.com)
 *
 *    This source code is free software; you can redistribute it
 *    and/or modify it in source code form under the terms of the GNU
 *    General Public License as published by the Free Software
 *    Foundation; either version 2 of the License, or (at your option)
 *    any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
 * USA.
 */

#include <assert.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

#include "ivl_alloc.h"
#include "sys_priv.h"
#include "sys_readmem_lex.h"

char **search_list = NULL;
unsigned sl_count = 0;

static void get_mem_params(vpiHandle argv, vpiHandle callh, const char *name,
                           char **fname, vpiHandle *mitem,
                           vpiHandle *start_item, vpiHandle *stop_item) {
  /* Get the first parameter (file name). */
  *fname = get_filename(callh, name, vpi_scan(argv));

  /* Get the second parameter (memory). */
  *mitem = vpi_scan(argv);

  /* Get optional third parameter (start address). */
  *start_item = vpi_scan(argv);
  if (*start_item) {
    /* Warn the user if they gave a real value for the start
     * address. */
    switch (vpi_get(vpiType, *start_item)) {
      case vpiConstant:
      case vpiParameter:
        if (vpi_get(vpiConstType, *start_item) != vpiRealConst) break;
        // fallthrough
      case vpiRealVar:
        vpi_printf("WARNING: %s:%d: ", vpi_get_str(vpiFile, callh),
                   (int)vpi_get(vpiLineNo, callh));
        vpi_printf(
            "%s's third argument (start address) is a real "
            "value.\n",
            name);
    }

    /* Get optional fourth parameter (finish address). */
    *stop_item = vpi_scan(argv);
    if (*stop_item) {
      /* Warn the user if they gave a real value for the finish
       * address. */
      switch (vpi_get(vpiType, *stop_item)) {
        case vpiConstant:
        case vpiParameter:
          if (vpi_get(vpiConstType, *stop_item) != vpiRealConst) {
            break;
          }
          // fallthrough
        case vpiRealVar:
          vpi_printf("WARNING: %s:%d: ", vpi_get_str(vpiFile, callh),
                     (int)vpi_get(vpiLineNo, callh));
          vpi_printf(
              "%s's fourth argument (finish address) is a "
              "real value.\n",
              name);
      }
      vpi_free_object(argv);
    }
  } else {
    *stop_item = 0;
  }
}

static int process_params(vpiHandle mitem, vpiHandle start_item,
                          vpiHandle stop_item, vpiHandle callh,
                          const char *name, int *start_addr, int *stop_addr,
                          int *addr_incr, int *min_addr, int *max_addr) {
  s_vpi_value val;
  int left_addr, right_addr;

  /* Get left addr of memory */
  val.format = vpiIntVal;
  vpi_get_value(vpi_handle(vpiLeftRange, mitem), &val);
  left_addr = val.value.integer;

  /* Get right addr of memory */
  val.format = vpiIntVal;
  vpi_get_value(vpi_handle(vpiRightRange, mitem), &val);
  right_addr = val.value.integer;

  /* Get start_addr, stop_addr and addr_incr */
  if (!start_item) {
    *start_addr = left_addr < right_addr ? left_addr : right_addr;
    *stop_addr = left_addr < right_addr ? right_addr : left_addr;
    *addr_incr = 1;
  } else {
    val.format = vpiIntVal;
    vpi_get_value(start_item, &val);
    *start_addr = val.value.integer;

    if (!stop_item) {
      *stop_addr = left_addr < right_addr ? right_addr : left_addr;
      *addr_incr = 1;
    } else {
      val.format = vpiIntVal;
      vpi_get_value(stop_item, &val);
      *stop_addr = val.value.integer;

      *addr_incr = *start_addr < *stop_addr ? 1 : -1;
    }
  }

  /* Find the minimum and maximum address. */
  *min_addr = *start_addr < *stop_addr ? *start_addr : *stop_addr;
  *max_addr = *start_addr < *stop_addr ? *stop_addr : *start_addr;

  /* If the range is not fully specified and the left address is
   * greater than the right address. Print a warning that this
   * code follows 1364-2005.
   *
   * If we passed a generation flag we could do the correct thing
   * for 1364-1995 and 1364-2001 instead of this general warning
   * or we could only show the warning when using 2001/1995.
   */
  if (!stop_item && (left_addr > right_addr)) {
    vpi_printf("WARNING: %s:%d: ", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
    vpi_printf("%s: Standard inconsistency, following 1364-2005.\n", name);
  }

  /* Check that start_addr and stop_addr are within the memory
     range */
  if (left_addr < right_addr) {
    if (*start_addr < left_addr || *start_addr > right_addr) {
      vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
                 (int)vpi_get(vpiLineNo, callh));
      vpi_printf(
          "%s: Start address %d is out of bounds for memory "
          "\'%s[%d:%d]\'!\n",
          name, *start_addr, vpi_get_str(vpiFullName, mitem), left_addr,
          right_addr);
      return 1;
    }

    if (*stop_addr < left_addr || *stop_addr > right_addr) {
      vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
                 (int)vpi_get(vpiLineNo, callh));
      vpi_printf(
          "%s: Finish address %d is out of bounds for memory "
          "\'%s[%d:%d]\'!\n",
          name, *stop_addr, vpi_get_str(vpiFullName, mitem), left_addr,
          right_addr);
      return 1;
    }
  } else {
    if (*start_addr < right_addr || *start_addr > left_addr) {
      vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
                 (int)vpi_get(vpiLineNo, callh));
      vpi_printf(
          "%s: Start address %d is out of bounds for memory "
          "\'%s[%d:%d]\'!\n",
          name, *start_addr, vpi_get_str(vpiFullName, mitem), left_addr,
          right_addr);
      return 1;
    }

    if (*stop_addr < right_addr || *stop_addr > left_addr) {
      vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
                 (int)vpi_get(vpiLineNo, callh));
      vpi_printf(
          "%s: Finish address %d is out of bounds for memory "
          "\'%s[%d:%d]\'!\n",
          name, *stop_addr, vpi_get_str(vpiFullName, mitem), left_addr,
          right_addr);
      return 1;
    }
  }
  return 0;
}

static PLI_INT32 sys_mem_compiletf(ICARUS_VPI_CONST PLI_BYTE8 *name) {
  vpiHandle callh = vpi_handle(vpiSysTfCall, 0);
  vpiHandle argv = vpi_iterate(vpiArgument, callh);
  vpiHandle arg;

  /* Check that there is a file name argument. */
  if (argv == 0) {
    vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
    vpi_printf("%s requires two arguments.\n", name);
    vpi_control(vpiFinish, 1);
    return 0;
  }
  if (!is_string_obj(vpi_scan(argv))) {
    vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
    vpi_printf("%s's first argument must be a file name (string).\n", name);
    vpi_control(vpiFinish, 1);
  }

  /* Check that there is a memory argument. */
  arg = vpi_scan(argv);
  if (!arg) {
    vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
    vpi_printf("%s requires a second (memory) argument.\n", name);
    vpi_control(vpiFinish, 1);
    return 0;
  }

  if (vpi_get(vpiType, arg) != vpiMemory) {
    vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
    vpi_printf("%s's second argument must be a memory.\n", name);
    vpi_control(vpiFinish, 1);
  }

  /* Check if there is a starting address argument. */
  arg = vpi_scan(argv);
  if (!arg) return 0;

  if (!is_numeric_obj(arg)) {
    vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
    vpi_printf(
        "%s's third argument must be a start address "
        "(numeric).\n",
        name);
    vpi_control(vpiFinish, 1);
  }

  /* Check if there is a finish address argument. */
  arg = vpi_scan(argv);
  if (!arg) return 0;

  if (!is_numeric_obj(arg)) {
    vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
    vpi_printf(
        "%s's fourth argument must be a finish address "
        "(numeric).\n",
        name);
    vpi_control(vpiFinish, 1);
  }

  /* Make sure there are no extra arguments. */
  check_for_extra_args(argv, callh, name, "four arguments", 1);

  return 0;
}

static PLI_INT32 sys_readmem_calltf(ICARUS_VPI_CONST PLI_BYTE8 *name) {
  int code, wwid, addr;
  FILE *file;
  char *fname = 0;
  s_vpi_value value;
  vpiHandle callh = vpi_handle(vpiSysTfCall, 0);
  vpiHandle argv = vpi_iterate(vpiArgument, callh);
  vpiHandle mitem = 0;
  vpiHandle start_item = 0;
  vpiHandle stop_item = 0;

  /* start_addr and stop_addr are the parameters given to $readmem in the
     Verilog code. When not specified, start_addr is equal to the lower of
     the [left,right]_addr and stop_addr is equal to the higher of the
     [left,right]_addr. */
  int start_addr, stop_addr, addr_incr;

  /* min_addr and max_addr are equal to start_addr and stop_addr if
     start_addr<stop_addr or vice versa if not... */
  int min_addr, max_addr;

  /* This is the number of words that we need from the memory. */
  unsigned word_count;

  /*======================================== Get parameters */

  get_mem_params(argv, callh, name, &fname, &mitem, &start_item, &stop_item);
  if (fname == 0) return 0;

  /*======================================== Process parameters */

  if (process_params(mitem, start_item, stop_item, callh, name, &start_addr,
                     &stop_addr, &addr_incr, &min_addr, &max_addr)) {
    free(fname);
    return 0;
  }

  /* Open the data file. */
  file = fopen(fname, "r");
  /* Check to see if we have other directories to look for this file. */
  if (file == 0 && sl_count > 0 && fname[0] != '/') {
    unsigned idx;
    char path[4096];

    for (idx = 0; idx < sl_count; idx += 1) {
      snprintf(path, sizeof(path), "%s/%s", search_list[idx], fname);
      path[sizeof(path) - 1] = 0;
      if ((file = fopen(path, "r"))) break;
    }
  }
  if (file == 0) {
    vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
    vpi_printf("%s: Unable to open %s for reading.\n", name, fname);
    free(fname);
    return 0;
  }

  /* We need this many words from the file. */
  word_count = max_addr - min_addr + 1;

  wwid = vpi_get(vpiSize, vpi_handle_by_index(mitem, min_addr));

  /* variable that will be used by the lexer to pass values
     back to this code */
  value.format = vpiVectorVal;
  value.value.vector = calloc((wwid + 31) / 32, sizeof(s_vpi_vecval));

  /* Configure the readmem lexer */
  if (strcmp(name, "$readmemb") == 0)
    sys_readmem_start_file(callh, file, 1, wwid, value.value.vector);
  else
    sys_readmem_start_file(callh, file, 0, wwid, value.value.vector);

  /*======================================== Read memory file */

  /* Run through the input file and store the new contents in the memory */
  addr = start_addr;
  while ((code = readmemlex()) != 0) {
    switch (code) {
      case MEM_ADDRESS:
        addr = value.value.vector->aval;
        if (addr < min_addr || addr > max_addr) {
          vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
                     (int)vpi_get(vpiLineNo, callh));
          vpi_printf(
              "%s(%s): address (0x%x) is out of range "
              "[0x%x:0x%x]\n",
              name, fname, addr, start_addr, stop_addr);
          goto bailout;
        }
        /* if there is an address in the memory file, then
           turn off any possible warnings about not having
           enough words to load the memory. This is standard
           behavior from 1364-2005. */
        word_count = 0;
        break;

      case MEM_WORD:
        if (addr >= min_addr && addr <= max_addr) {
          vpiHandle word_index;
          word_index = vpi_handle_by_index(mitem, addr);
          assert(word_index);
          vpi_put_value(word_index, &value, 0, vpiNoDelay);

          if (word_count > 0) word_count -= 1;
        } else {
          vpi_printf("WARNING: %s:%d: ", vpi_get_str(vpiFile, callh),
                     (int)vpi_get(vpiLineNo, callh));
          vpi_printf(
              "%s(%s): Too many words in the file for the "
              "requested range [%d:%d].\n",
              name, fname, start_addr, stop_addr);
          goto bailout;
        }

        addr += addr_incr;
        break;

      case MEM_ERROR:
        vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
                   (int)vpi_get(vpiLineNo, callh));
        vpi_printf("%s(%s): Invalid input character: %s\n", name, fname,
                   readmem_error_token);
        goto bailout;
        break;

      default:
        assert(0);
        break;
    }
  }

  /* Print a warning if there are not enough words in the data file. */
  if (word_count > 0) {
    vpi_printf("WARNING: %s:%d: ", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
    vpi_printf(
        "%s(%s): Not enough words in the file for the "
        "requested range [%d:%d].\n",
        name, fname, start_addr, stop_addr);
  }

bailout:
  free(value.value.vector);
  free(fname);
  fclose(file);
  destroy_readmem_lexor();
  return 0;
}

static PLI_INT32 free_readmempath(p_cb_data cb_data) {
  unsigned idx;

  (void)cb_data; /* Parameter is not used. */
  for (idx = 0; idx < sl_count; idx += 1) {
    free(search_list[idx]);
  }
  free(search_list);
  search_list = NULL;
  sl_count = 0;
  return 0;
}

static PLI_INT32 sys_readmempath_calltf(ICARUS_VPI_CONST PLI_BYTE8 *name) {
  vpiHandle callh = vpi_handle(vpiSysTfCall, 0);
  vpiHandle argv = vpi_iterate(vpiArgument, callh);
  vpiHandle paths = vpi_scan(argv);
  s_vpi_value val;
  unsigned len, idx;
  char *path;

  vpi_free_object(argv);

  /* Get the search path string. */
  val.format = vpiStringVal;
  vpi_get_value(paths, &val);

  /* Verify that we have a string and that it is not NULL. */
  if (val.format != vpiStringVal || !*(val.value.str)) {
    vpi_printf("WARNING: %s:%d: ", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
    vpi_printf("%s's argument (%s) is not a valid string.\n", name,
               vpi_get_str(vpiType, paths));
    return 0;
  }

  /*
   * Verify that the search path is composed of only printable
   * characters.
   */
  len = strlen(val.value.str);
  for (idx = 0; idx < len; idx++) {
    if (!isprint((int)val.value.str[idx])) {
      char msg[64];
      char *esc_path = as_escaped(val.value.str);
      snprintf(msg, sizeof(msg), "WARNING: %s:%d:", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
      msg[sizeof(msg) - 1] = 0;
      vpi_printf(
          "%s %s's argument contains non-printable "
          "characters.\n",
          msg, name);
      vpi_printf("%*s \"%s\"\n", (int)strlen(msg), " ", esc_path);
      free(esc_path);
      return 0;
    }
  }

  /* Clear the old list before creating the new list. */
  free_readmempath(NULL);

  /*
   * Break the string into individual paths and add them to the list.
   * Print a warning if the path is not valid.
   */
  for (path = strtok(val.value.str, ":"); path; path = strtok(NULL, ":")) {
    int res;
    struct stat sb;

    /* Warn the user if the path is not valid. */
    res = stat(path, &sb);
    if (res == 0) {
      if (!S_ISDIR(sb.st_mode)) {
        vpi_printf("WARNING: %s:%d: ", vpi_get_str(vpiFile, callh),
                   (int)vpi_get(vpiLineNo, callh));
        vpi_printf(
            "%s's path element \"%s\" is not a "
            "directory!\n",
            name, path);
        continue;
      }
    } else {
      vpi_printf("WARNING: %s:%d: ", vpi_get_str(vpiFile, callh),
                 (int)vpi_get(vpiLineNo, callh));
      vpi_printf("%s could not find directory \"%s\"!\n", name, path);
      continue;
    }

    /* Add a valid search path element to the list. */
    sl_count += 1;
    search_list = (char **)realloc(search_list, sizeof(char **) * sl_count);
    search_list[sl_count - 1] = strdup(path);
  }

  return 0;
}

static PLI_INT32 sys_writemem_calltf(ICARUS_VPI_CONST PLI_BYTE8 *name) {
  int addr;
  FILE *file;
  char *fname = 0;
  unsigned cnt;
  s_vpi_value value;
  vpiHandle callh = vpi_handle(vpiSysTfCall, 0);
  vpiHandle argv = vpi_iterate(vpiArgument, callh);
  vpiHandle mitem = 0;
  vpiHandle start_item = 0;
  vpiHandle stop_item = 0;

  int start_addr, stop_addr, addr_incr;
  int min_addr, max_addr;  // Not used in this routine.

  /*======================================== Get parameters */

  get_mem_params(argv, callh, name, &fname, &mitem, &start_item, &stop_item);

  if (fname == 0) return 0;

  /*======================================== Process parameters */

  if (process_params(mitem, start_item, stop_item, callh, name, &start_addr,
                     &stop_addr, &addr_incr, &min_addr, &max_addr)) {
    free(fname);
    return 0;
  }

  /* Open the data file. */
  file = fopen(fname, "w");
  if (file == 0) {
    vpi_printf("ERROR: %s:%d: ", vpi_get_str(vpiFile, callh),
               (int)vpi_get(vpiLineNo, callh));
    vpi_printf("%s: Unable to open %s for writing.\n", name, fname);
    free(fname);
    return 0;
  }

  if (strcmp(name, "$writememb") == 0)
    value.format = vpiBinStrVal;
  else
    value.format = vpiHexStrVal;

  /*======================================== Write memory file */

  cnt = 0;
  for (addr = start_addr; addr != stop_addr + addr_incr;
       addr += addr_incr, ++cnt) {
    vpiHandle word_index;

    if (cnt % 16 == 0) fprintf(file, "// 0x%08x\n", cnt);

    word_index = vpi_handle_by_index(mitem, addr);
    assert(word_index);
    vpi_get_value(word_index, &value);
    fprintf(file, "%s\n", value.value.str);
  }

  fclose(file);
  free(fname);
  return 0;
}

void sys_readmem_register(void) {
  s_vpi_systf_data tf_data;
  vpiHandle res;
  s_cb_data cb_data;

  tf_data.type = vpiSysTask;
  tf_data.tfname = "$readmemh";
  tf_data.calltf = sys_readmem_calltf;
  tf_data.compiletf = sys_mem_compiletf;
  tf_data.sizetf = 0;
  tf_data.user_data = "$readmemh";
  res = vpi_register_systf(&tf_data);
  vpip_make_systf_system_defined(res);

  tf_data.type = vpiSysTask;
  tf_data.tfname = "$readmemb";
  tf_data.calltf = sys_readmem_calltf;
  tf_data.compiletf = sys_mem_compiletf;
  tf_data.sizetf = 0;
  tf_data.user_data = "$readmemb";
  res = vpi_register_systf(&tf_data);
  vpip_make_systf_system_defined(res);

  tf_data.type = vpiSysTask;
  tf_data.tfname = "$readmempath";
  tf_data.calltf = sys_readmempath_calltf;
  tf_data.compiletf = sys_one_string_arg_compiletf;
  tf_data.sizetf = 0;
  tf_data.user_data = "$readmempath";
  res = vpi_register_systf(&tf_data);
  vpip_make_systf_system_defined(res);

  tf_data.type = vpiSysTask;
  tf_data.tfname = "$writememh";
  tf_data.calltf = sys_writemem_calltf;
  tf_data.compiletf = sys_mem_compiletf;
  tf_data.sizetf = 0;
  tf_data.user_data = "$writememh";
  res = vpi_register_systf(&tf_data);
  vpip_make_systf_system_defined(res);

  tf_data.type = vpiSysTask;
  tf_data.tfname = "$writememb";
  tf_data.calltf = sys_writemem_calltf;
  tf_data.compiletf = sys_mem_compiletf;
  tf_data.sizetf = 0;
  tf_data.user_data = "$writememb";
  res = vpi_register_systf(&tf_data);
  vpip_make_systf_system_defined(res);

  cb_data.reason = cbEndOfSimulation;
  cb_data.time = 0;
  cb_data.cb_rtn = free_readmempath;
  cb_data.user_data = "system";
  vpi_register_cb(&cb_data);
}
